﻿// Copyright (c) SimpleIdServer. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;

namespace SimpleIdServer.OpenIdConnect
{
    public class CustomOpenIdConnectConfigureOptions : IConfigureNamedOptions<CustomOpenIdConnectOptions>
    {
        private readonly IAuthenticationConfigurationProvider _authenticationConfigurationProvider;
        private static readonly Func<string, TimeSpan> _invariantTimeSpanParse = (string timespanString) => TimeSpan.Parse(timespanString, CultureInfo.InvariantCulture);
        private static readonly Func<string, TimeSpan?> _invariantNullableTimeSpanParse = (string timespanString) => TimeSpan.Parse(timespanString, CultureInfo.InvariantCulture);

        /// <summary>
        /// Initializes a new <see cref="OpenIdConnectConfigureOptions"/> given the configuration
        /// provided by the <paramref name="configurationProvider"/>.
        /// </summary>
        /// <param name="configurationProvider">An <see cref="IAuthenticationConfigurationProvider"/> instance.</param>
        public CustomOpenIdConnectConfigureOptions(IAuthenticationConfigurationProvider configurationProvider)
        {
            _authenticationConfigurationProvider = configurationProvider;
        }

        /// <inheritdoc />
        public void Configure(string? name, CustomOpenIdConnectOptions options)
        {
            if (string.IsNullOrEmpty(name))
            {
                return;
            }

            var configSection = _authenticationConfigurationProvider.GetSchemeConfiguration(name);

            if (configSection is null || !configSection.GetChildren().Any())
            {
                return;
            }

            options.AccessDeniedPath = new PathString(configSection[nameof(options.AccessDeniedPath)] ?? options.AccessDeniedPath.Value);
            options.Authority = configSection[nameof(options.Authority)] ?? options.Authority;
            options.AutomaticRefreshInterval = StringHelpers.ParseValueOrDefault(configSection[nameof(options.AutomaticRefreshInterval)], _invariantTimeSpanParse, options.AutomaticRefreshInterval);
            options.BackchannelTimeout = StringHelpers.ParseValueOrDefault(configSection[nameof(options.BackchannelTimeout)], _invariantTimeSpanParse, options.BackchannelTimeout);
            options.CallbackPath = new PathString(configSection[nameof(options.CallbackPath)] ?? options.CallbackPath.Value);
            options.ClaimsIssuer = configSection[nameof(options.ClaimsIssuer)] ?? options.ClaimsIssuer;
            options.ClientId = configSection[nameof(options.ClientId)] ?? options.ClientId;
            options.ClientSecret = configSection[nameof(options.ClientSecret)] ?? options.ClientSecret;
            SetCookieFromConfig(configSection.GetSection(nameof(options.CorrelationCookie)), options.CorrelationCookie);
            options.DisableTelemetry = StringHelpers.ParseValueOrDefault(configSection[nameof(options.DisableTelemetry)], bool.Parse, options.DisableTelemetry);
            options.ForwardAuthenticate = configSection[nameof(options.ForwardAuthenticate)] ?? options.ForwardAuthenticate;
            options.ForwardChallenge = configSection[nameof(options.ForwardChallenge)] ?? options.ForwardChallenge;
            options.ForwardDefault = configSection[nameof(options.ForwardDefault)] ?? options.ForwardDefault;
            options.ForwardForbid = configSection[nameof(options.ForwardForbid)] ?? options.ForwardForbid;
            options.ForwardSignIn = configSection[nameof(options.ForwardSignIn)] ?? options.ForwardSignIn;
            options.ForwardSignOut = configSection[nameof(options.ForwardSignOut)] ?? options.ForwardSignOut;
            options.GetClaimsFromUserInfoEndpoint = StringHelpers.ParseValueOrDefault(configSection[nameof(options.GetClaimsFromUserInfoEndpoint)], bool.Parse, options.GetClaimsFromUserInfoEndpoint);
            options.MapInboundClaims = StringHelpers.ParseValueOrDefault(configSection[nameof(options.MapInboundClaims)], bool.Parse, options.MapInboundClaims);
            options.MaxAge = StringHelpers.ParseValueOrDefault(configSection[nameof(options.MaxAge)], _invariantNullableTimeSpanParse, options.MaxAge);
            options.MetadataAddress = configSection[nameof(options.MetadataAddress)] ?? options.MetadataAddress;
            SetCookieFromConfig(configSection.GetSection(nameof(options.NonceCookie)), options.NonceCookie);
            options.Prompt = configSection[nameof(options.Prompt)] ?? options.Prompt;
            options.RefreshInterval = StringHelpers.ParseValueOrDefault(configSection[nameof(options.RefreshInterval)], _invariantTimeSpanParse, options.RefreshInterval);
            options.RefreshOnIssuerKeyNotFound = StringHelpers.ParseValueOrDefault(configSection[nameof(options.RefreshOnIssuerKeyNotFound)], bool.Parse, options.RefreshOnIssuerKeyNotFound);
            options.RemoteAuthenticationTimeout = StringHelpers.ParseValueOrDefault(configSection[nameof(options.RemoteAuthenticationTimeout)], _invariantTimeSpanParse, options.RemoteAuthenticationTimeout);
            options.RemoteSignOutPath = new PathString(configSection[nameof(options.RemoteSignOutPath)] ?? options.RemoteSignOutPath.Value);
            options.RequireHttpsMetadata = StringHelpers.ParseValueOrDefault(configSection[nameof(options.RequireHttpsMetadata)], bool.Parse, options.RequireHttpsMetadata);
            options.Resource = configSection[nameof(options.Resource)] ?? options.Resource;
            options.ResponseMode = configSection[nameof(options.ResponseMode)] ?? options.ResponseMode;
            options.ResponseType = configSection[nameof(options.ResponseType)] ?? options.ResponseType;
            options.ReturnUrlParameter = configSection[nameof(options.ReturnUrlParameter)] ?? options.ReturnUrlParameter;
            options.SaveTokens = StringHelpers.ParseValueOrDefault(configSection[nameof(options.SaveTokens)], bool.Parse, options.SaveTokens);
            ClearAndSetListOption(options.Scope, configSection.GetSection(nameof(options.Scope)));
            options.SignedOutCallbackPath = new PathString(configSection[nameof(options.SignedOutCallbackPath)] ?? options.SignedOutCallbackPath.Value);
            options.SignedOutRedirectUri = configSection[nameof(options.SignedOutRedirectUri)] ?? options.SignedOutRedirectUri;
            options.SignInScheme = configSection[nameof(options.SignInScheme)] ?? options.SignInScheme;
            options.SignOutScheme = configSection[nameof(options.SignOutScheme)] ?? options.SignOutScheme;
            options.SkipUnrecognizedRequests = StringHelpers.ParseValueOrDefault(configSection[nameof(options.SkipUnrecognizedRequests)], bool.Parse, options.SkipUnrecognizedRequests);
            options.UsePkce = StringHelpers.ParseValueOrDefault(configSection[nameof(options.UsePkce)], bool.Parse, options.UsePkce);
            options.UseTokenLifetime = StringHelpers.ParseValueOrDefault(configSection[nameof(options.UseTokenLifetime)], bool.Parse, options.UseTokenLifetime);
        }

        private static void SetCookieFromConfig(IConfiguration cookieConfigSection, CookieBuilder cookieBuilder)
        {
            if (cookieConfigSection is null || !cookieConfigSection.GetChildren().Any())
            {
                return;
            }

            // Override the existing defaults when values are set instead of constructing
            // an entirely new CookieBuilder.
            cookieBuilder.Domain = cookieConfigSection[nameof(cookieBuilder.Domain)] ?? cookieBuilder.Domain;
            cookieBuilder.HttpOnly = StringHelpers.ParseValueOrDefault(cookieConfigSection[nameof(cookieBuilder.HttpOnly)], bool.Parse, cookieBuilder.HttpOnly);
            cookieBuilder.IsEssential = StringHelpers.ParseValueOrDefault(cookieConfigSection[nameof(cookieBuilder.IsEssential)], bool.Parse, cookieBuilder.IsEssential);
            cookieBuilder.Expiration = StringHelpers.ParseValueOrDefault(cookieConfigSection[nameof(cookieBuilder.Expiration)], _invariantNullableTimeSpanParse, cookieBuilder.Expiration);
            cookieBuilder.MaxAge = StringHelpers.ParseValueOrDefault<TimeSpan?>(cookieConfigSection[nameof(cookieBuilder.MaxAge)], _invariantNullableTimeSpanParse, cookieBuilder.MaxAge);
            cookieBuilder.Name = cookieConfigSection[nameof(CookieBuilder.Name)] ?? cookieBuilder.Name;
            cookieBuilder.Path = cookieConfigSection[nameof(CookieBuilder.Path)] ?? cookieBuilder.Path;
            cookieBuilder.SameSite = cookieConfigSection[nameof(CookieBuilder.SameSite)] is string sameSiteMode
                ? Enum.Parse<SameSiteMode>(sameSiteMode, ignoreCase: true)
                : cookieBuilder.SameSite;
            cookieBuilder.SecurePolicy = cookieConfigSection[nameof(CookieBuilder.SecurePolicy)] is string securePolicy
                ? Enum.Parse<CookieSecurePolicy>(securePolicy, ignoreCase: true)
                : cookieBuilder.SecurePolicy;
            ClearAndSetListOption(cookieBuilder.Extensions, cookieConfigSection.GetSection(nameof(cookieBuilder.Extensions)));
        }

        private static void ClearAndSetListOption(ICollection<string> listOption, IConfigurationSection listConfigSection)
        {
            var elementsFromConfig = listConfigSection.GetChildren().Select(element => element.Value).OfType<string>();
            if (elementsFromConfig.Any())
            {
                listOption.Clear();
                foreach (var element in elementsFromConfig)
                {
                    listOption.Add(element);
                }
            }
        }

        /// <inheritdoc />
        public void Configure(CustomOpenIdConnectOptions options)
        {
            Configure(Options.DefaultName, options);
        }
    }
}
